import {
  useAsyncData,
  useFetch,
  useLazyAsyncData,
  useLazyFetch,
} from 'nuxt/app';
import { reactive, ref, watch } from 'vue';

import { createSseClient } from '../../client-core/bundle/serverSentEvents';
import type { HttpMethod } from '../../client-core/bundle/types';
import type { Client, Config, RequestOptions } from './types';
import {
  buildUrl,
  createConfig,
  executeFetchFn,
  mergeConfigs,
  mergeHeaders,
  mergeInterceptors,
  serializeBody,
  setAuthParams,
  unwrapRefs,
} from './utils';

export const createClient = (config: Config = {}): Client => {
  let _config = mergeConfigs(createConfig(), config);

  const getConfig = (): Config => ({ ..._config });

  const setConfig = (config: Config): Config => {
    _config = mergeConfigs(_config, config);
    return getConfig();
  };

  const beforeRequest = async (options: RequestOptions) => {
    const opts = {
      ..._config,
      ...options,
      $fetch: options.$fetch ?? _config.$fetch ?? $fetch,
      headers: mergeHeaders(_config.headers, options.headers),
      onRequest: mergeInterceptors(_config.onRequest, options.onRequest),
      onResponse: mergeInterceptors(_config.onResponse, options.onResponse),
    };

    if (opts.security) {
      await setAuthParams({
        ...opts,
        security: opts.security,
      });
    }

    if (opts.requestValidator) {
      await opts.requestValidator(opts);
    }

    const url = buildUrl(opts);

    return { opts, url };
  };

  const request: Client['request'] = ({
    asyncDataOptions,
    composable = '$fetch',
    ...options
  }) => {
    const key = options.key;
    const opts = {
      ..._config,
      ...options,
      $fetch: options.$fetch ?? _config.$fetch ?? $fetch,
      headers: mergeHeaders(_config.headers, options.headers),
      onRequest: mergeInterceptors(_config.onRequest, options.onRequest),
      onResponse: mergeInterceptors(_config.onResponse, options.onResponse),
    };

    const {
      requestValidator,
      responseTransformer,
      responseValidator,
      security,
    } = opts;
    if (requestValidator || security) {
      // auth must happen in interceptors otherwise we'd need to require
      // asyncContext enabled
      // https://nuxt.com/docs/guide/going-further/experimental-features#asynccontext
      opts.onRequest = [
        async ({ options }) => {
          if (security) {
            await setAuthParams({
              auth: opts.auth,
              headers: options.headers,
              query: options.query,
              security,
            });
          }

          if (requestValidator) {
            await requestValidator({
              ...options,
              // @ts-expect-error
              body: options.rawBody,
            });
          }
        },
        ...opts.onRequest,
      ];
    }

    if (responseTransformer || responseValidator) {
      opts.onResponse = [
        ...opts.onResponse,
        async ({ options, response }) => {
          if (options.responseType && options.responseType !== 'json') {
            return;
          }

          if (!response.ok) {
            return;
          }

          if (responseValidator) {
            await responseValidator(response._data);
          }

          if (responseTransformer) {
            response._data = await responseTransformer(response._data);
          }
        },
      ];
    }

    // remove Content-Type header if body is empty to avoid sending invalid requests
    if (opts.body === undefined || opts.body === '') {
      opts.headers.delete('Content-Type');
    }

    const fetchFn = opts.$fetch;

    if (composable === '$fetch') {
      return executeFetchFn(
        // @ts-expect-error
        opts,
        fetchFn,
      );
    }

    if (composable === 'useFetch' || composable === 'useLazyFetch') {
      opts.rawBody = opts.body;
      const bodyParams = reactive({
        body: opts.body,
        bodySerializer: opts.bodySerializer,
      });
      const body = ref(serializeBody(opts));
      opts.body = body;
      watch(bodyParams, (changed) => {
        body.value = serializeBody(changed);
      });
      return composable === 'useLazyFetch'
        ? useLazyFetch(() => buildUrl(opts), opts)
        : useFetch(() => buildUrl(opts), opts);
    }

    const handler: any = () =>
      executeFetchFn(
        // @ts-expect-error
        opts,
        fetchFn,
      );

    if (composable === 'useAsyncData') {
      return key
        ? useAsyncData(key, handler, asyncDataOptions)
        : useAsyncData(handler, asyncDataOptions);
    }

    if (composable === 'useLazyAsyncData') {
      return key
        ? useLazyAsyncData(key, handler, asyncDataOptions)
        : useLazyAsyncData(handler, asyncDataOptions);
    }

    return undefined as any;
  };

  const makeMethodFn =
    (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
      request({ ...options, method });

  const makeSseFn =
    (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
      const { opts, url } = await beforeRequest(options);
      return createSseClient({
        ...unwrapRefs(opts),
        body: opts.body as BodyInit | null | undefined,
        method,
        onRequest: undefined,
        signal: unwrapRefs(opts.signal) as AbortSignal,
        url,
      });
    };

  return {
    buildUrl,
    connect: makeMethodFn('CONNECT'),
    delete: makeMethodFn('DELETE'),
    get: makeMethodFn('GET'),
    getConfig,
    head: makeMethodFn('HEAD'),
    options: makeMethodFn('OPTIONS'),
    patch: makeMethodFn('PATCH'),
    post: makeMethodFn('POST'),
    put: makeMethodFn('PUT'),
    request,
    setConfig,
    sse: {
      connect: makeSseFn('CONNECT'),
      delete: makeSseFn('DELETE'),
      get: makeSseFn('GET'),
      head: makeSseFn('HEAD'),
      options: makeSseFn('OPTIONS'),
      patch: makeSseFn('PATCH'),
      post: makeSseFn('POST'),
      put: makeSseFn('PUT'),
      trace: makeSseFn('TRACE'),
    },
    trace: makeMethodFn('TRACE'),
  } as Client;
};
